在前端这个迅猛发展的领域,前端请求方法也是迭出不穷,从原生 XHR
到 jQuery ajax
,再到现在的 axios
和 fetch
,那发请求的最佳实践是什么呢?这个答案我们在文章中会慢慢给出
在分析 axios
封装方法之前,我们先来看看 jQuery ajax
和 fetch
方法
jQuery ajax
jQuery ajax
是对原生 XHR
的封装,还支持 JSONP
$.ajax({
type: 'POST',
url: url,
data: data,
dataType: dataType,
success: function() {},
error: function() {}
})
但是随着 react
,vue
等前端框架的兴起,jQuery
早已不复当年之勇
且 jQuery
整个项目太大,单纯使用 ajax
却要引入整个 jQuery
非常的不合理,于是便有了 fetch
的解决方案
fetch
fetch
号称是 ajax
的替代品,它的 API
是基于 Promise
设计的
而且 fetch
也是 whatwg 的标准,具体可以参考 fetch Living Standard
// 原生 XHR
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText) // 从服务器获取数据
}
}
xhr.send()
// fetch
fetch(url).then(response => {
if (response.ok) {
response.json()
}
})
.then(data => console.log(data))
.catch(err => console.log(err))
再搭配上 async
/ await
将会让我们的异步代码更加优雅
async function test() {
let response = await fetch(url)
let data = await response.json()
console.log(data)
}
fetch
相对于是更加底层的,很多情况下都需要我们再次封装,比如需要手动将参数拼接成 'name=test' 的格式,而 jquery ajax
已经封装好了
// jquery ajax
$.post(url, {name: 'test'})
// fetch
fetch(url, {
method: 'POST',
body: Object.keys({name: 'test'}).map((key) => {
return `${encodeURIComponent(key)} = ${encodeURIComponent(params[key])}`
}).join('&')
})
除此之外,fetch
还有很多问题,比如
-
fetch
只对网络请求报错,对 400,500 都当做成功的请求,需要封装去处理 -
fetch
不支持取消(最大的问题),不支持超时控制,使用setTimeout
及Promise.reject
实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费 -
fetch
没有办法原生监测请求的进度,而XHR
可以
具体问题可以参考 fetch 没有你想象的那么美 和 fetch 使用的常见问题及解决方法
所以 fetch 并不是开箱即用的
axios
对比于 jQuery ajax
、 fetch
、 axios
的优点简直不要太多,它也是 vue
官网推荐使用的
axios
是一个基于 Promise
的 HTTP
库,可以用于浏览器和 nodejs
中,主要特性有
- 从浏览器中创建
XMLHttpRequest
- 从
node.js
创建http
请求 - 支持
Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换
JSON
数据 - 客户端支持防御
XSRF
axios
本质上也是对原生 XHR
的封装
axios.post('/upload', {
userName: 'zhangsan'
})
.then(res => {
console.log(res, res.data)
})
.catch(err => {
console.log(err)
})
对于客户端支持防御 XSRF,实际上就是让你的每个请求都带一个从 cookie
中拿到的 key
,根据浏览器同源策略,假冒的网站是拿不到你 cookie
中的 key
,这样后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略
axios
既提供了并发的封装方法,也没有 fetch
的各种问题,而且体积也较小,当之无愧现在最应该选用的请求的方式
axios 支持同构, 对于既要支持 SPA 同时支持 SSR 的项目来说非常好用
具体的使用方法我们可以查看 axios 中文文档
下面是一些封装方法的详细使用
合并请求
let q1 = axios.post('/add', 'b=2')
let q2 = axios.post('/upload', 'a=1')
// 合并这两个请求,并处理其成功和失败
// 一般会用于两个分开的相关联的请求,缺一不可
axios.all([q1, q2])
.then(axios.spread((res1, res2) => {
// 全部成功
console.log(res1, res2)
}))
.catch(err => {
// 其一失败
console.log(err)
})
options 参数
// axios.headers = {} // 覆盖原本默认头
// axios.defalut.headers.accept = 'abc' // 走默认头,修改个例
// 请求一
axios.get('/', {
params: {id: 1},
// transformResponse 在传递给 then/catch 前,允许修改响应数据
transformResponse: (data) => {
console.log(data)
// 就是 res.data,可以更改
data = 'data changed'
return data
}
})
.then(res => {
console.log(res, res.data)
})
.catch(err => {
console.log(err)
})
// 请求二
axios.post('/upload', 'name=jack', {
timeout: 1000,
// transformRequest 允许在向服务器发送前,修改请求数据
// 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
transformRequest: (data) => {
// 加工请求体数据
return 'name = rose'
}
})
.then(res => {
console.log(res, res.data)
})
.catch(err => {
console.log(err)
})
取消请求
这里我们以上传文件为例,在 vuejs 中使用
choose file : <input type = "file" name = "file" @change = "changeFile" />
<button @click = "sendAjax">sendReq</button>
<button @click = "cancelAjax">cancelReq</button>
<button @click = "resumeAjax">ContinueSendlReq</button>
// 发送请求
sendAjax() {
// 在请求前需要先生成独立的标识,才能更方便取消请求
const CancelToken = axios.CancelToken // 获取取消标识
const source = CancelToken.source() // 使用 CancelToken.source 工厂方法创建 cancel token
// 保存起来方便取消调用
this.source = source
let fd = new FormData()
fd.append('file', this.file)
axios.post('/uploadfile', fd, {
// 携带取消标识
cancelToken: source.token,
onUploadProgress: (progressEvent) => {
console.log(progressEvent.loaded)
console.log(progressEvent.total)
// 保存最后上传大小
this.loaded = progressEvent.loaded
this.rate = (progressEvent.loaded / progressEvent.total) * 100
}
})
.then(res => {
console.log(res.data)
})
.catch(err => {
console.log(err)
})
},
// 取消请求
cancelAjax() {
this.source.cancel() // 通过内部存好的方法做取消
},
// 继续请求
resumeAjax() {
// 剪裁文件 开始 结尾
const fileData = this.file.slice(this.loaded + 1, this.file.size)
let fd = new FormData()
fd.append('file', fileData)
// 为了后续续传以后,再取消
const CancelToken = axios.CancelToken // 获取取消标识
const source = CancelToken.source() // 使用 CancelToken.source 工厂方法创建 cancel token
this.source = source
axios.post('/uploadfile', fd, {
// 携带取消标识
cancelToken: source.token,
onUploadProgress: (progressEvent) => {
// 保存最后上传大小
this.loaded = progressEvent.loaded
this.rate = (progressEvent.loaded / progressEvent.total) * 100
}
})
.then(res => {
console.log(res.data)
})
.catch(err => {
console.log(err)
})
},
// 上传文件
changeFile(e) {
// console.log(e.target.files[0])
this.file = e.target.files[0]
}
为了更方便的取消请求,我们在发送请求时,需要有独立的标识
取消请求大概来说分为三步
- 获取请求标识
- 使用
CancelToken.source
工厂方法创建cancel
token
,保存起来方便取消调用 - 通过内部存好的方法做取消
拦截器
在请求或响应被 then
或 catch
处理前拦截它们
比如实现一个功能如下
- 在请求发起之前,show 一个 loading 出来
- 响应回来之后,关闭这个 loading
可以通过设置一个标识表示 loading 出现与否,默认是不出现,在在发送请求之前将 loading 标识设为 true,拿到响应数据之后,将 loading 标识设为 false
data() {
return {
isshow: false // loading默认不出现
}
},
添加请求拦截器
// 配置请求拦截器 use 给请求之前做的事可以是多件,可以 use 多次
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么
console.log(config)
this.isshow = true // loading 出现
return config
}, (error) => {
return Promise.reject(error)
})
添加响应拦截器
axios.interceptors.response.use((res) => {
// 对响应数据做点什么
console.log(res) // res.config
this.isshow = false // loading 消失
return res
}, (error) => {
return Promise.reject(error)
})
在拦截器中我们也可以实现一个类似 cookie 的机制,比如现在广泛使用的 jwt,通过客户端保存数据,再在请求中带回服务器
- 服务器发送 cookie 到客户端保存起来,在响应拦截器中完成
- 然后在请求之前,从本地获取 cookie,设置请求头拦截器
在服务端代码中设置响应头中包含 token: 'abc'
的信息
server.post('/token', function (req, res){
console.log('body', req.body)
// 给客户端一个标识
res.set('token', 'abc')
res.send({token: 'abc', msg: 'post请求成功'})
})
然后前端请求数据,通过响应拦截器将 token 存在本地,使用 localStorage
axios.interceptors.response.use((res) => {
// 获取服务器的响应头
if(res.headers.token) {
var token = res.headers.token
localStorage.setItem('token', token)
}
return res
}, (error) => {
return Promise.reject(error)
})
然后再次发送请求时,通过请求拦截器从本地取出 token 放入请求头中
axios.interceptors.request.use((config) => {
// 设置请求头,类似cookie
var token = localStorage.getItem('token')
if(token) {
config.headers['token'] = token
}
return config
}, (error) => {
return Promise.reject(error)
})
nodejs 使用
在 node 中,我们可能遇到需要向另一个服务器 post 数据的需求,这个时候如果使用 axios 会非常方便,毕竟有原生支持的 Promise 语法
const querystring = require('querystring')
axios.post('http://xxx.com/', querystring.stringify({ userName: 'zhangsan' }))
其他的方法使用这里就不做过多赘述了,具体的使用可以查看 axios 中文文档
所以,发请求的最佳实践是什么呢?综上,当之无愧现在最应该选用的请求的方式就是 axios
上述用到的所有代码完整版可以查看 axios-demo